interface ElType {
  tag: string | null;
  attrs?: object;
  html?: string;
  children?: ElType[] | string;
  ref?: (e: HTMLElement, node: ElType) => void;
  on?: { [key: string]: EventListener };
}

export type ViewData = {
  src: string;
}

export type Options = {
  container?: HTMLElement;
  style?: Object;
  step?: number;
  canEscClose?: boolean;
  toolbars?: string[] | string[][],
  className?: string;
}

function setCss (el: HTMLElement, style: object) {
  for (const k in style) {
    el.style[k] = style[k]
  }
}

function elm (options: ElType | HTMLElement): HTMLElement | Text {
  if (options instanceof HTMLElement) return options
  if (options.tag === null && typeof options.children === 'string') return document.createTextNode(options.children)

  const el = document.createElement(options.tag)

  for (const key in options.attrs) {
    if (key === 'style') {
      if (options.attrs[key]) setCss(el, options.attrs[key])
    } else if (key === 'className') {
      el.className = isArr(options.attrs[key]) ? options.attrs[key].filter(e => !!e).join(' ') : options.attrs[key]
    } else {
      el[key] = options.attrs[key]
    }
  }

  if (typeof options.children === 'object') {
    for (const i in options.children) {
      el.appendChild(elm.apply(null, [options.children[i]]))
    }
  } else if (typeof options.children !== 'undefined') {
    el.appendChild(document.createTextNode(options.children))
  }

  if (options.html) {
    el.innerHTML = options.html
  }

  if (isFun(options.ref)) {
    options.ref(el, options)
  }

  return el
}

function isFun (v: any) {
  return typeof v === 'function'
}

function isArr (v: any) {
  return Object.prototype.toString.call(v) === '[object Array]'
}

function countTo (from: number, to: number, fn: (n: number) => void) {
  function loop () {
    const speed = to - from

    from = from + speed - (speed - speed / 32)
    fn(from)

    if (speed > 0 && speed > 0.01) {
      requestAnimationFrame(loop)
    } else if (speed < 0 && speed < -0.01) {
      requestAnimationFrame(loop)
    } else {
      fn(to)
      // console.log(from, speed, to)
    }
  }

  loop()
}

const arrowIcon = (className: string) => `<svg viewBox="0 0 35 35" fill="currentColor" class="${className}"><path d="M9.4,13.9c-0.2,0.2-0.2,0.6,0,0.8l8.1,8.1l0,0l0,0l8.1-8.1c0.2-0.2,0.2-0.6,0-0.8l-1.3-1.3 c-0.2-0.2-0.6-0.2-0.8,0l-5.5,5.5c-0.2,0.2-0.6,0.2-0.8,0l-5.5-5.5c-0.2-0.2-0.6-0.2-0.8,0L9.4,13.9z"></path></svg>`

export default class ImageViewer {
  container: HTMLElement;
  box: HTMLElement;
  viewBox: HTMLElement;
  rateBox: HTMLElement;
  closeBtn: HTMLElement;
  buttonBox: HTMLElement;
  ratio: number;
  doing: boolean;
  step: number;
  currentIndex: number;
  canEscClose: boolean;
  viewList: {
    data: ViewData,
    el: HTMLElement | null
  }[];
  private _clock: any = null

  private listens: {
    el: Document | HTMLElement;
    name: string;
    handle: (e: any) => void;
  }[] = [];

  private toolbars = {
    'zoom-in': {
      el: elm({ tag: 'button', attrs: { className: 'bt bt-zoom-in' }, children: '+' }),
      handle: () => this.toRatio(this.ratio + this.step)
    },
    'zoom-out': {
      el: elm({ tag: 'button', attrs: { className: 'bt bt-zoom-out' }, children: '-' }),
      handle: () => this.toRatio(this.ratio - this.step)
    },
    prev: {
      el: elm({ tag: 'button', attrs: { className: 'bt bt-prev' }, html: arrowIcon('arrow-icon left') }),
      handle: () => this.prev()
    },
    next: {
      el: elm({ tag: 'button', attrs: { className: 'bt bt-next' }, html: arrowIcon('arrow-icon right') }),
      handle: () => this.next()
    },
    reset: {
      el: elm({ tag: 'button', attrs: { className: 'bt bt-reset' }, children: '1:1' }),
      handle: () => this.reset()
    }
  }

  constructor (viewList: ViewData[], options: Options = {}) {
    this.box = elm({ tag: 'div', attrs: { className: ['image-viewer', options.className], style: options.style } }) as HTMLElement
    this.viewBox = elm({ tag: 'div', attrs: { className: 'view-box' }}) as HTMLElement
    this.rateBox = elm({ tag: 'span', attrs: { className: 'rate-text' }}) as HTMLElement
    this.closeBtn = elm({ tag: 'button', attrs: { className: 'control-close' }, children: '×'}) as HTMLElement
    this.viewList = []
    this.ratio = 0
    this.doing = false
    this.canEscClose = options.canEscClose
    this.step = options.step || 0.15
    this.container = options.container || document.body
    this.viewList = viewList.map(e => ({ data: e, el: null }))
    const toolbars = options.toolbars || ['zoom-in', 'zoom-out', ['prev', 'reset', 'next']]

    if (toolbars.length > 0) {
      const children = toolbars.map(e => {
        if (isArr(e)) {
          return elm({
            tag: 'div',
            attrs: { className: 'bt-group' },
            children: (<string[]>e).map(e => {
              const item = this.toolbars[e]
              item.el.addEventListener('click', item.handle, false)
              return item.el
            })
          })
        }
  
        const item = this.toolbars[(e as string)]
        item.el.addEventListener('click', item.handle, false)
        return item.el
      })

      this.buttonBox = elm({ tag: 'div', attrs: { className: 'toolbar' }, children: children}) as HTMLElement
    }

    this.init()
    this.view(0)
  }

  private init () {
    const that = this
    // 记录初始位置
    const pos = {
      start: { x: 0, y: 0, matrix: [1, 0, 0, 1, 0, 0] },
      down: false
    }

    // mousewheel
    this.listens.push({
      el: this.box,
      name: 'mousewheel',
      handle: e => {
        const type = e.type
  
        // firfox 往下滚 e.detail 正值，其他负值
        if (type === 'DOMMouseScroll' || type === 'mousewheel') {
          // 统一往下滚e.delta是负值
          e.delta = e.wheelDelta ? e.wheelDelta / 120 : -(e.detail || 0) / 3
        }

        let ratio = that.ratio
        // up
        if (e.delta > 0) {
          ratio += that.step
        } else {
          ratio -= that.step
        }

        ratio = ratio < 0 ? 0 : ratio
        that.toRatio(ratio)
      }
    })

    // mousedown
    this.listens.push({
      el: this.viewBox,
      name: 'mousedown',
      handle: e => {
        pos.start.x = e.clientX
        pos.start.y = e.clientY
        pos.down = true
        const matrix = this.viewBox.style.transform

        if (matrix) {
          pos.start.matrix = matrix.match(/-?\d?\.?\d+/g).map(e => Number(e))
        }
      }
    })

    // mouseup
    this.listens.push({
      el: this.box,
      name: 'mouseup',
      handle: (e) => {
        pos.down = false
      }
    })
    // viewBox move
    this.listens.push({
      el: this.box,
      name: 'mousemove',
      handle: (e) => {
        e.preventDefault()

        if (!pos.down) return
        const matrix = [...pos.start.matrix]
        matrix[4] = e.clientX - pos.start.x + matrix[4]
        matrix[5] = e.clientY - pos.start.y + matrix[5]

        this.viewBox.style.transform = `matrix(${matrix.join(',')})`
      }
    })

    // esc close
    this.listens.push({
      el: document,
      name: 'keydown',
      handle: (e) => {
        if (e.keyCode === 27 && this.canEscClose) this.destroy()
      }
    })

    // close btn
    this.listens.push({
      el: this.closeBtn,
      name: 'click',
      handle: () => this.destroy()
    })

    this.listen()

    this.box.appendChild(this.viewBox)
    this.box.appendChild(this.rateBox)
    this.box.appendChild(this.closeBtn)
    this.box.appendChild(this.buttonBox)
    this.container.appendChild(this.box)
  }

  private view (index: number) {
    // if (this.doing) return false
    const current = this.viewList[this.currentIndex]
    const currentIndex = index < 0 ? 0 : (index > this.viewList.length - 1) ? this.viewList.length - 1 : index
    const item = this.viewList[currentIndex]

    if (this.currentIndex !== index && current && current.el) {
      current.el.remove()
    }

    this.currentIndex = currentIndex

    if (item.el) {
      this.viewBox.appendChild(item.el)
      this.toRatio(1)
    } else {
      const image = new Image()
      image.onload = () => {
        image.width = image.width
        image.height = image.height
        this.viewBox.appendChild(image)
        image.style.transform = `scale(${this.ratio})`
        this.toRatio(1)
      }

      item.el = image
      image.src = item.data.src
    }
  }

  toRatio (ratio: number, done?: () => void) {
    // if (this.doing) return false

    const that = this
    this.doing = true
    this.rateBox.classList.add('show')

    countTo(this.ratio, ratio, (to) => {
      that.viewList[this.currentIndex].el.style.transform = `scale(${to})`
      that.rateBox.innerText = (to * 100).toFixed(0) + '%'
      this.ratio = to
      clearTimeout(this._clock)

      if (to === ratio) {
        this.doing = false
        if (isFun(done)) done()

        this._clock = setTimeout(() => {
          this.rateBox.classList.remove('show')
        }, 1500)
      }
    })
  }

  close (done: () => void)  {
    this.toRatio(0, () => {
      this.box.classList.add('close')

      if (isFun(done)) done()
    })
  }

  show ()  {
    this.box.classList.remove('close')
    this.toRatio(1)
  }

  next () {
    this.view(this.currentIndex + 1)
    this.reset()
  }

  prev () {
    this.view(this.currentIndex - 1)
    this.reset()
  }

  // 复位
  reset () {
    this.viewBox.style.transform = 'matrix(1, 0, 0, 1, 0, 0)'
    this.toRatio(1)
  }

  destroy () {
    this.removeListen()
    this.box.remove()
  }

  listen () {
    for (const i in this.listens) {
      const item = this.listens[i]
      item.el.addEventListener(item.name, item.handle, false)
    }
  }

  removeListen () {
    for (const i in this.listens) {
      const item = this.listens[i]
      item.el.removeEventListener(item.name, item.handle, false)
    }

    for (const i in this.toolbars) {
      this.toolbars[i].el.removeEventListener('click', this.toolbars[i].handle, false)
    }
  }
}
